package index;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;

import tools.Normalizer;

/**
 * La classe DocumentAnalyser contient une liste des fonctions pour l'analyse
 * des documents.
 * 
 */
public class DocumentAnalyser implements  Serializable {

	public HashMap<String, Integer> getDfs() {
		return dfs;
	}

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private String directory;

	// nombre de documents dans le corpus
	private int nbDocs;

	private Normalizer normalizer;
	private boolean removeStopWords;

	// un map de <terme,DocumentFrequency>
	private HashMap<String, Integer> dfs;
	// un Map de maximum de Term Frequency des termes <docName,TF>
	private HashMap<String, Integer> maxTf;
	// le maximum de idf pour tous les termes du corpus
	private float maxIdf;

	public DocumentAnalyser(String corpus, Normalizer norm,
			boolean removeStopWords) {
		this.directory = corpus;
		this.normalizer = norm;
		this.removeStopWords = removeStopWords;
		try {
			this.nbDocs = this.calculeNombreDocument(directory);
			this.dfs = this.calculeDocumentFrequency(this.directory);
			this.maxIdf = this.calculeMaxIDF();
			this.maxTf = this.calculeTFs(directory);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Obtenir le nombre de documents contenus dans un repertoire
	 * 
	 * @param dir
	 * @return
	 */
	private int calculeNombreDocument(String dirName) {
		int nbDocs = 0;
		File f = new File(dirName);
		String[] filenames = f.list();

		for (String file : filenames) {
			f = new File(dirName + file);
			if (f.isDirectory())
				nbDocs += calculeNombreDocument(dirName + file + '/');
			else
				nbDocs++;
		}
		return nbDocs;
	}

	/*
	 * private String formatDirectory(String dirName){
	 * 
	 * if(dirName.charAt(dirName.length()-1) != '/' ) dirName += '/'; return
	 * dirName; }
	 */

	/**
	 * calculer Document Frequency pour chaque terme dans un corpus
	 * 
	 * @param dirName
	 * @param normalizer
	 * @param removeStopWords
	 * @return
	 * @throws IOException
	 */
	private HashMap<String, Integer> calculeDocumentFrequency(String dirName)
			throws IOException {
		File dir = new File(dirName);
		if (dirName.charAt(dirName.length() - 1) != '/')
			dirName += '/';
		String[] filenames = dir.list();

		String encoding = "ISO-8859-1";
		HashMap<String, Integer> result = new HashMap<String, Integer>();
		File f;
		for (String file : filenames) {
			f = new File(dirName + file);
			if (f.isDirectory())
				result.putAll(calculeDocumentFrequency(dirName + file));
			else {
				ArrayList<String> words = normalizer.normalize(dirName + file,
						encoding, removeStopWords);
				ArrayList<String> wordsSimple = new ArrayList<String>();

				// Eliminer les mots qui apparaissent plusieurs fois
				for (String word : words) {
					if (!wordsSimple.contains(word)) {
						wordsSimple.add(word);
					}
				}

				// Mise a jour de la HashMap
				for (String word : wordsSimple) {
					if (result.containsKey(word)) {
						result.put(word, result.get(word) + 1);
					} else {
						result.put(word, 1);
					}
				}

				// surponderation du titre
				result = augmenterTitre(dirName + file, result, 30, encoding);

			}
		}

		return result;
	}

	private HashMap<String, Integer> augmenterTitre(String file, HashMap<String, Integer> result,
			int facteur, String encoding) throws IOException {

		BufferedReader bf = new BufferedReader(new FileReader(file));
		bf.readLine();
		String title = bf.readLine();
		if (!title.startsWith("TITLE=")) {
			return result;
		}
		title = title.substring(6).toLowerCase();

		String[] words = title.split(" ");

		// Mise a jour de la HashMap
		for (String word : words) {
			if (word.length() > 0 && Character.isLetter(word.charAt(0))) {
				if (result.containsKey(word)) {
					result.put(word, result.get(word) + facteur);
				} else {
					result.put(word, facteur);
				}
			}
		}
		return result;
	}

	/**
	 * Obtenir le maximum de Term Frequency pour un doc
	 * 
	 * @param fileName
	 *            le nom du document
	 * @param normalizer
	 * @param removeStopWords
	 * @return
	 * @throws IOException
	 */
	private int calculeMaxTF(String fileName) throws IOException {
		int result = 0;
		ArrayList<String> listeMots = normalizer.normalize(fileName,
				"ISO-8859-1", removeStopWords);
		HashMap<String, Integer> mapOccur = new HashMap<String, Integer>();

		/* Calculer la frequence de chaque mot dans le document */
		for (String mot : listeMots) {
			if (mapOccur.containsKey(mot)) {
				int freq = mapOccur.get(mot);
				mapOccur.put(mot, freq + 1);
			} else {
				mapOccur.put(mot, 1);
			}
		}

		for (String mot : mapOccur.keySet()) {
			if (mapOccur.get(mot) > result) {
				result = mapOccur.get(mot);
			}
		}

		return result;
	}

	/**
	 * Calculer le max de TF pour chaque doc
	 * 
	 * @param dirName
	 * @return
	 */
	private HashMap<String, Integer> calculeTFs(String dirName) {
		HashMap<String, Integer> tfs = new HashMap<String, Integer>();
		File f = new File(dirName);
		String[] filenames = f.list();

		for (String file : filenames) {
			f = new File(dirName + file);
			if (f.isDirectory())
				tfs.putAll(calculeTFs(dirName + file + '/'));
			else {
				try {
					tfs.put(dirName + file, this.calculeMaxTF(dirName + file));
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		return tfs;
	}

	/**
	 * Calculer le maximum de IDF de tous les termes dans le corpus
	 * 
	 * @return le max de IDF
	 */
	private float calculeMaxIDF() {

		// on cherche le mim de DF
		int minDF = Integer.MAX_VALUE;
		for (String mot : dfs.keySet()) {
			if (dfs.get(mot) < minDF) {
				minDF = dfs.get(mot);
			}
		}
		return (float) Math.log(this.getNombreDocument() / minDF);
	}

	/**
	 * Normaliser le document en utilisant le normaliser choisi
	 * 
	 * @param chemin
	 * @param norm
	 *            trois types de normailiser a choisir: Tokenizer, Stemmer,
	 *            TreeTagger
	 * @return
	 * @throws IOException
	 */
	public ArrayList<String> normaliser(String chemin, Normalizer norm)
			throws IOException {
		return norm.normalize(chemin);
	}

	public int getNombreDocument() {
		return this.nbDocs;
	}

	public HashMap<String, Integer> getDocumentFrequency() {
		return this.dfs;
	}

	public float getMaxIDF() {
		return this.maxIdf;
	}

	public HashMap<String, Integer> getMaxTFs() {
		return this.maxTf;
	}

	/**
	 * Obtenir le Tf-idf (weight) de chaque terme dans un document
	 * 
	 * @param fileName
	 * @param dfs
	 * @param documentNumber
	 * @param normalizer
	 * @param removeStopWords
	 * @return
	 * @throws IOException
	 */
	public HashMap<String, Float> getTfIdf(String fileName,
			HashMap<String, Integer> dfs) throws IOException {
		HashMap<String, Float> result = new HashMap<String, Float>();
		ArrayList<String> listeMots = normalizer.normalize(fileName,
				"ISO-8859-1", removeStopWords);
		HashMap<String, Integer> mapOccur = new HashMap<String, Integer>();

		/* Calculer la frequence de chaque mot dans le document */
		for (String mot : listeMots) {
			if (mapOccur.containsKey(mot)) {
				int freq = mapOccur.get(mot);
				mapOccur.put(mot, freq + 1);
			} else {
				mapOccur.put(mot, 1);
			}
		}

		/* Calculer le tf-idf de chaque term dans le document doc */
		Set<String> terms = mapOccur.keySet();
		for (String mot : terms) {
			int dft = dfs.get(mot);
			int tf = mapOccur.get(mot);

			float wtd = (float) (tf * Math.log(nbDocs / dft));
			result.put(mot, wtd);
		}

		return result;
	}

	/**
	 * Calculer le tf-idf de chaque terme de chaque document du corpus et puis
	 * ecrire le resultat dans des documents .poids
	 * 
	 * @param inDirName
	 *            chemin du corpus
	 * @param outDirName
	 *            chemin du répertoire qui contient le résultat
	 * @param normalizer
	 * @param removeStopWords
	 *            boolean
	 * @throws IOException
	 */
	public void getWeightFiles(String inDirName, String outDirName)
			throws IOException {
		File dir = new File(inDirName);
		String[] filenames = dir.list();
		HashMap<String, Integer> dfs = calculeDocumentFrequency(inDirName);

		File outDir = new File(outDirName);
		if (outDir.mkdir()) {
			System.out
					.println("Ajout du dossier : " + outDir.getAbsolutePath());
		} else {
			System.err.println("Echec sur le dossier : "
					+ outDir.getAbsolutePath());
		}

		/*
		 * Pour chaque fichier, calculer le tf-idf pour chaque mot et ecrire le
		 * resultat dans un fichier
		 */
		for (String file : filenames) {
			String newName = file.replace(".txt", ".poids");
			File res = new File(outDir, newName);

			boolean existe = res.createNewFile();
			HashMap<String, Float> tfidf = getTfIdf(inDirName + file, dfs);

			if (!existe) {
				System.out.println("File already exists.");
				System.exit(0);
			} else {
				FileWriter fstream = new FileWriter(outDirName + "/" + newName);
				PrintWriter out = new PrintWriter(fstream);

				Set<String> set = tfidf.keySet();
				ArrayList<String> list = new ArrayList<String>(set);
				Collections.sort(list);
				for (String mot : list) {
					out.println(mot + "\t" + tfidf.get(mot));
				}
				out.close();
			}
		}
	}

	/**
	 * Dans une nouvelle classe Java, écrivez une méthode dont les deux
	 * arguments sont deux noms de fichiers \texttt{.poids} créés au TD
	 * précédent et représentant les termes pondérés contenus dans un
	 * document. La méthode devra renvoyer le score de similarité entre les
	 * deux documents en utilisant la formule ci-dessus. Rappel : format du
	 * fichier .poids : Deux colonnes séparées par une tabulation. Première
	 * colonne : le mot Seconde colonne : le poids de ce mot dans le document
	 * 
	 * @param fileName1
	 * @param fileName2
	 * @return la similarité entre les deux fichiers.
	 */
	public double getSimilarity(String fileName1, String fileName2) {
		HashMap<String, Double> weights1 = new HashMap<String, Double>();
		double denominateur1 = 0.0;
		double denominateur2 = 0.0;
		double numerateur = 0.0;
		Double weight1;
		Double weight2;

		try {
			// lecture du premier fichier
			// on met le tout dans une hashmap
			InputStream ips = new FileInputStream(fileName1);
			InputStreamReader ipsr = new InputStreamReader(ips);
			BufferedReader br = new BufferedReader(ipsr);
			String line;
			while ((line = br.readLine()) != null) {
				String[] fields = line.split("\t");
				if (fields.length != 2) {
					throw new RuntimeException(
							"Erreur de format dans le fichier de poids "
									+ fileName1 + " , ligne : " + line);
				}
				try {
					weight1 = Double.parseDouble(fields[1]);
					weights1.put(fields[0], weight1);
					// dénominateur 1
					denominateur1 += weight1 * weight1;
				} catch (NumberFormatException e) {
					throw new RuntimeException(
							"Erreur de format dans le fichier de poids "
									+ fileName1 + " , ligne : " + line);
				}
			}
			br.close();
			// lecture du second fichier
			// on fait les calculs au fur et à mesure
			ips = new FileInputStream(fileName2);
			ipsr = new InputStreamReader(ips);
			br = new BufferedReader(ipsr);
			while ((line = br.readLine()) != null) {
				String[] fields = line.split("\t");
				if (fields.length != 2) {
					throw new RuntimeException(
							"Erreur de format dans le fichier de poids "
									+ fileName2 + " , ligne : " + line);
				}
				try {
					// calcul de la similarité
					// numérateur :
					weight1 = weights1.get(fields[0]);
					weight2 = Double.parseDouble(fields[1]);
					if (weight1 != null) {
						numerateur += weight1 * weight2;
					}
					// dénominateur 2
					denominateur2 += weight2 * weight2;
				} catch (NumberFormatException e) {
					throw new RuntimeException(
							"Erreur de format dans le fichier de poids "
									+ fileName2 + " , ligne : " + line);
				}
			}
			br.close();
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}
		// calcul final de la similarité
		if (numerateur == 0) {
			return 0;
		} else {
			return numerateur
					/ (Math.sqrt(denominateur1) * Math.sqrt(denominateur2));
		}
	}

	/**
	 * Ecrivez ensuite une méthode affichant les scores de similarité entre un
	 * document (dont le fichier .poids est passé en premier argument) et
	 * l'ensemble des documents du corpus. Le second argument représente le
	 * répertoire contenant l'ensemble des fichiers .poids du corpus. La liste
	 * devra être affichée par ordre décroissant de similarité des
	 * documents.
	 * 
	 * @param fileName
	 * @param dirName
	 */
	public void getSimilarDocuments(String fileName, String dirName) {
		File dir = new File(dirName);
		if (dir.isDirectory()) {
			// Liste des fichiers du répertoire
			// ajouter un filtre (FileNameFilter) sur les noms
			// des fichiers si nécessaire
			String[] similarFileNames = dir.list();

			// la liste des scores de similarité
			final HashMap<String, Double> scores = new HashMap<String, Double>();
			// Pour optimiser il faudrait calculer les parties concernant
			// le fichier seul une seule fois, mais ce n'est pas très important
			for (String similarFileName : similarFileNames) {
				scores.put(
						similarFileName,
						getSimilarity(fileName, dirName + File.separator
								+ similarFileName));
			}
			// on ordonne
			TreeSet<String> files = new TreeSet<String>(
					new Comparator<String>() {

						@Override
						public int compare(String o1, String o2) {
							// on veut classer dans l'ordre DESCENDANT
							// (donc ordre inverse)
							return Double.compare(scores.get(o2),
									scores.get(o1));
						}

					});
			files.addAll(scores.keySet());
			// on affiche !
			for (String file : files) {
				System.out.println(file + " : " + scores.get(file));
			}
		} else {
			throw new RuntimeException(dirName + " n'est pas un repertoire !");
		}
	}

}
